package com.googlecode.afx.spring.aop;

import java.lang.reflect.Method;

import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Node;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.SuppressAjWarnings;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.core.annotation.AnnotationUtils;

import com.googlecode.afx.AFXComponentCache;
import com.googlecode.afx.VoidMethodCallback;
import com.googlecode.afx.annotation.AFXAction;
import com.googlecode.afx.annotation.AFXView;
import com.googlecode.afx.utils.AFXExpressionUtils;
import com.googlecode.afx.utils.AFXUtils;
import com.googlecode.afx.utils.ReflectionUtils;
import com.googlecode.afx.view.AbstractErrorHandlingView;
import com.googlecode.afx.view.AbstractView;
import com.googlecode.afx.view.View;
import com.googlecode.afx.view.binding.DatabindingUtils;
import com.googlecode.afx.view.validation.ValidationException;

/**
 * Aspect intercepting all <tt>@AFXAction</tt> annotated methods.
 * 
 * @author MartinKoster
 *
 */
@Aspect
@Configurable
public class AFXControllerInterceptor {

	private static final Log LOG = LogFactory.getLog(AFXControllerInterceptor.class);

	@Autowired
	private AFXComponentCache jfxComponentCache;

	/**
	 * Point-cut targeting at all public methods.
	 */
	@Pointcut("execution(public * *(..))")
	public void anyPublicMethod() {
	}
	
	/**
	 * Point-cut targetting at all class carrying a @AFXController annotation.
	 */
	@Pointcut("within(@com.googlecode.afx.annotation.AFXController *)")
	public void anyClassWithJFXController() {}	

	/**
	 * Around method woven around all public controller methods.
	 * 
	 * @param pjp
	 * @return
	 * @throws Throwable
	 */
	@SuppressAjWarnings("adviceDidNotMatch")
	@Around("anyClassWithJFXController() && anyPublicMethod() && @annotation(com.googlecode.afx.annotation.AFXAction)")
	public Object handlePublicControllerMethod(final ProceedingJoinPoint pjp) throws Throwable {

		final Object controller = pjp.getTarget();
		final Method method = ((MethodSignature) pjp.getStaticPart().getSignature()).getMethod();

		AFXAction jfxAction = AnnotationUtils.findAnnotation(method, AFXAction.class);
		AFXView jfxView = AnnotationUtils.findAnnotation(controller.getClass(), AFXView.class);

		View view = null;
		if (jfxView != null) {
			view = this.lookupView(jfxView.id());
		}
		if(jfxAction != null) {
			return this.handleJFXAction(pjp, controller, method, jfxAction, view);
		} else {
			return this.handleNonJFXAction(pjp, controller, method, jfxAction, view);
		}

	}

	/**
	 * Handler method that handles controller methods annotated by <tt>@AFXAction</tt>.
	 * 
	 * @param pjp
	 * @param controller
	 * @param method
	 * @param jfxAction
	 * @param view
	 * @return
	 * @throws Throwable
	 */
	public Object handleJFXAction(final ProceedingJoinPoint pjp, Object controller, Method method, AFXAction jfxAction, View view) throws Throwable {
		Object retVal = null;
		if (jfxAction.asynchronous()) {
			this.executeJoinPointAsynchronously(pjp, controller, method, jfxAction, view);
		} else {
			this.executeJoinPointSynchronously(pjp, controller, method, jfxAction, view);
		}
		return retVal;		
	}	

	/**
	 * Handler method that handles all controller methods that are <b>not</b> annotated by <tt>@AFXAction</tt>. 
	 * <p>
	 * This methods takes care that changes in the model are reflected also in the UI.
	 * 
	 * @param pjp
	 * @param controller
	 * @param method
	 * @param jfxAction
	 * @param view
	 * @return
	 * @throws Throwable
	 */
	public Object handleNonJFXAction(final ProceedingJoinPoint pjp, Object controller, Method method, AFXAction jfxAction, View view) throws Throwable {
		Object retVal = null;
		retVal = pjp.proceed();
		return retVal;
	}	
	
	/**
	 * Execute the given <code>ProceedingJointPoint</code> in an asynchronous
	 * fashion.
	 * 
	 * @param pjp
	 * @param controller
	 * @param method
	 * @param jfxAction
	 * @throws Throwable
	 */
	public void executeJoinPointAsynchronously(final ProceedingJoinPoint pjp, final Object controller,
			final Method method, final AFXAction jfxAction, final View view) throws Throwable {

		final Node progressNode = this.determineProgressNode(jfxAction);

		this.invokeInNewThread(() -> {

			
				// handle onBefore event
				this.handleOnBefore(jfxAction, controller);

				// handle validation (throws a ValidationException, if
				// validation failed)
				this.handleValidation(jfxAction, view);

				try {
					
					// execute actual surrounded method
					LOG.debug("Invoking method '" + controller.getClass().getName() + "." + method.getName()
							+ "' asynchronosly in new thread.");
					
					pjp.proceed();
					
					// refresh data binding ()
					//this.refreshBindings(view);
					
				} catch (Throwable e) {
					if (e instanceof Exception) {
						throw (Exception) e;
					} else {
						throw new Exception(e);
					}
				}

				// handle onAfter event
				this.handleOnAfter(jfxAction, controller);

			}, jfxAction, progressNode);

	}

	/**
	 * Execute the given <code>ProceedingJointPoint</code> in a synchronous
	 * fashion.
	 * 
	 * @param pjp
	 * @param controller
	 * @param method
	 * @param jfxAction
	 * @return
	 * @throws Exception
	 */
	public void executeJoinPointSynchronously(final ProceedingJoinPoint pjp, final Object controller,
			final Method method, final AFXAction jfxAction, final View view) throws Throwable {
		try {
			
			// handle onBefore event
			this.handleOnBefore(jfxAction, controller);

			// handle validation (throws a ValidationException, if validation
			// failed)
			this.handleValidation(jfxAction, view);

			// execute actual surrounded method
			LOG.debug("Invoking method '" + controller.getClass().getName() + "." + method.getName()
					+ "' synchronously in current thread.");

			pjp.proceed();

			// refresh data binding ()
			//this.refreshBindings(view);
			
			// handle onAfter event
			this.handleOnAfter(jfxAction, controller);

			// handleSuccessView
			this.handleOnSuccessView(jfxAction);

		} catch (Throwable throwable) {
			LOG.debug("", throwable);
			if (throwable instanceof Exception) {

				// handleFailureView
				this.handleOnFailureView(jfxAction, (Exception) throwable);
			} else {
				throw throwable;
			}
		}
	}

	/**
	 * Handles validation by evaluating the corresponding setting in annotation
	 * <tt>AFXAction</tt>.
	 * 
	 * @param jfxAction
	 * @param view
	 */
	protected void handleValidation(AFXAction jfxAction, View view) throws ValidationException {
		if (jfxAction.requiresSuccessfulValidation()) {
			if (view instanceof AbstractView) {
				AbstractView abstractView = (AbstractView) view;
				abstractView.getValidationHandler().throwValidationExceptionIfHasErrors();
			}
		}
	}

	/**
	 * Executes the code supplied in the <code>AsyncCallback</code> in a
	 * separate thread.
	 * 
	 * @param callback
	 */
	public void invokeInNewThread(final VoidMethodCallback callback, final AFXAction jfxAction, final Node progressNode) {

		Task<Void> task = new Task<Void>() {
			@Override
			protected Void call() throws Exception {
				callback.executeMethod();
				return null;
			}

			@Override
			protected void succeeded() {
				try {
					if (progressNode != null) {
						DatabindingUtils.unbindProgressProperty(progressNode);
					}
					invokeMethod(() -> handleOnSuccessView(jfxAction), true);
				} catch (Exception e) {
					LOG.error(e.getMessage(), e);
					if (e instanceof RuntimeException) {
						throw (RuntimeException) e;
					} else {
						throw new RuntimeException("", e);
					}
				}
			}

			@Override
			protected void cancelled() {
				if (progressNode != null) {
					DatabindingUtils.unbindProgressProperty(progressNode);
				}
			}

			@Override
			protected void failed() {
				try {
					if (progressNode != null) {
						DatabindingUtils.unbindProgressProperty(progressNode);
					}
					invokeMethod(() -> handleOnFailureView(jfxAction, this.getException()), true);
				} catch (Exception e) {
					LOG.error(e.getMessage(), e);
					if (e instanceof RuntimeException) {
						throw (RuntimeException) e;
					} else {
						throw new RuntimeException("", e);
					}
				}
			}
		};

		if (progressNode != null) {
			DatabindingUtils.bindTaskToProgressProperty(task, progressNode);
		}

		LOG.debug("Starting async task.");
		Thread thread = new Thread(task);
		thread.setDaemon(true);
		thread.start();
	}

	/**
	 * Executes the given <tt>VoidMethodCallback</tt> either in the current
	 * thread of execution or in the JavaFX thread using
	 * <code>Platform.runLater()</tt>.
	 * 
	 * @param callback
	 * @param useRunLater
	 */
	public static void invokeMethod(final VoidMethodCallback callback, boolean useRunLater) throws Exception {
		if (useRunLater) {
			Platform.runLater(AFXUtils.getWrapper(() -> {
				try {
					callback.executeMethod();
				} catch (Exception e) {
					LOG.error(e.getMessage(), e);
					if (e instanceof RuntimeException) {
						throw (RuntimeException) e;
					} else {
						throw new RuntimeException(e);
					}
				}
			}));
		} else {
			callback.executeMethod();
		}
	}

	/**
	 * Handle onBefore event, if supplied in the <code>AFXAction</code>
	 * annotation.
	 * 
	 * @param jfxAction
	 */
	protected void handleOnBefore(AFXAction jfxAction, Object controller) {
		if (StringUtils.trimToNull(jfxAction.onBefore()) != null) {
			Object target = this.determineComponent(jfxAction.onBefore(), controller);
			Method method = ReflectionUtils.findMethod(target.getClass(),
					this.determineMethodName(jfxAction.onBefore()));
			LOG.debug("onBefore: Invoking method '" + method.getName() + "' on class '" + target.getClass().getName()
					+ "'.");
			ReflectionUtils.invokeMethod(method, target);
		}
	}

	/**
	 * Handle onAfter event, if supplied in the <code>AFXAction</code>
	 * annotation.
	 * 
	 * @param jfxAction
	 */
	protected void handleOnAfter(AFXAction jfxAction, Object controller) {
		if (StringUtils.trimToNull(jfxAction.onAfter()) != null) {
			Object target = this.determineComponent(jfxAction.onAfter(), controller);
			Method method = ReflectionUtils
					.findMethod(target.getClass(), this.determineMethodName(jfxAction.onAfter()));
			LOG.debug("onAfter: Invoking method '" + method.getName() + "' on class '" + target.getClass().getName()
					+ "'.");
			ReflectionUtils.invokeMethod(method, target);
		}
	}

	/**
	 * Determines the node responsible for tracking the progress.
	 * 
	 * @param jfxAction
	 * @return
	 */
	protected Node determineProgressNode(AFXAction jfxAction) {
		Node progressNode = null;
		final View view = (View) this.determineComponent(jfxAction.progressIndicatorId(), null);
		final String nodeId = this.determineNodeId(jfxAction.progressIndicatorId());
		if (view != null) {
			progressNode = AFXUtils.findNodeById(view.getNode(), nodeId);
		}
		return progressNode;
	}

	/**
	 * Determines the correct component defined by the supplied <tt>path</tt>.
	 * If <tt>path</tt> is a nested path with dot-notation, the first name in
	 * the path is supposed to be bean name that is retrieved from the component
	 * cache.
	 * 
	 * @param path
	 * @param defaultComponent
	 * @return
	 */
	protected Object determineComponent(String path, Object defaultComponent) {
		Object retVal = defaultComponent;
		String baseName = AFXExpressionUtils.getComponentName(path);
		if (baseName != null) {
			retVal = this.jfxComponentCache.lookupComponent(baseName);
		}
		return retVal;
	}

	/**
	 * Determines the correct method name defined by the supplied <tt>path</tt>.
	 * If <tt>path</tt> is a nested path with dot-notation, the second name in
	 * the path is supposed to be the method name.
	 * 
	 * @param path
	 * @param controller
	 * @return
	 */
	protected String determineMethodName(String path) {
		String methodName = AFXExpressionUtils.getMethodName(path);
		return methodName;
	}

	/**
	 * Determines the correct node ID defined by the supplied <tt>path</tt>. If
	 * <tt>path</tt> is a nested path with dot-notation, the second name in the
	 * path is supposed to be the node ID.
	 * 
	 * @param path
	 * @param controller
	 * @return
	 */
	protected String determineNodeId(String path) {
		String nodeId = AFXExpressionUtils.getNodeId(path);
		return nodeId;
	}

	/**
	 * If the processing of the controller call was successful, this method is
	 * responsible for displaying the success view.
	 * 
	 * @param jfxAction
	 */
	protected void handleOnSuccessView(AFXAction jfxAction) {
		if (StringUtils.trimToNull(jfxAction.onSuccessView()) != null) {
			View view = this.lookupView(jfxAction.onSuccessView());
			LOG.debug("Displaying onSuccessView with ID='" + view.getId() + "'");
			view.show();
		}
	}

	/**
	 * In case an error in form of an <code>Exception</code> occurs, this method
	 * is responsible for displaying the error in the error view.
	 * 
	 * @param jfxAction
	 * @param exception
	 */
	protected void handleOnFailureView(AFXAction jfxAction, Throwable exception) {
		if (StringUtils.trimToNull(jfxAction.onFailureView()) != null) {
			View view = this.lookupView(jfxAction.onFailureView());
			LOG.debug("Displaying onFailureView with ID='" + view.getId() + "' for exception '"
					+ exception.getClass().getName() + "'.");
			if (view instanceof AbstractErrorHandlingView) {
				((AbstractErrorHandlingView) view).setThrowable(exception);
			}
			view.show();
		}
	}

	/**
	 * Retrieves a view from the <code>ViewCache</code>.
	 * 
	 * @param id
	 * @return
	 */
	protected View lookupView(String id) {
		Object candidate = this.jfxComponentCache.lookupComponent(id);
		if (candidate == null) {
			throw new IllegalStateException("No component with ID='" + id + "' has been found!");
		}
		if (!(candidate instanceof View)) {
			throw new IllegalStateException("component with ID='" + id + "' is not a view instance, type is '"
					+ candidate.getClass().getName() + "'!");
		}
		return (View) candidate;
	}

	public AFXComponentCache getJfxComponentCache() {
		return jfxComponentCache;
	}

	public void setJfxComponentCache(AFXComponentCache jfxComponentCache) {
		this.jfxComponentCache = jfxComponentCache;
	}

}
